package at.ac.tuwien.lsds.schedule.strategy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;

import org.apache.log4j.Logger;

import at.ac.tuwien.lsds.exception.MachineException;
import at.ac.tuwien.lsds.exception.SchedulerException;
import at.ac.tuwien.lsds.federation.CloudPartner;
import at.ac.tuwien.lsds.model.Application;
import at.ac.tuwien.lsds.model.PhysicalMachine;
import at.ac.tuwien.lsds.model.PhysicalMachineImpl;
import at.ac.tuwien.lsds.util.SchedulerOutputFile;
import at.ac.tuwien.lsds.util.Settings;

/**
 * These scheduling algorithm makes use of the bin packaging problem
 */
public class SchedulerD extends ScheduleStrategyImpl {
	private static final Logger log = Logger.getLogger(SchedulerD.class);
	private boolean reorganize = false;

	private Comparator<Application> appComp = new Comparator<Application>() {

		@Override
		public int compare(Application o1, Application o2) {
			return Integer.compare(o2.getCpu(), o1.getCpu());
		}
	};

	public SchedulerD() {
		pms = new ArrayList<PhysicalMachine>();
		queue = new ArrayList<Application>();

//		SchedulerOutputFile.getInstance().setScenario(Settings.getScenario().toString());
		SchedulerOutputFile.getInstance().setScheduler("D");
	}

	@Override
	public void runningTimeIncreased() {
		if(reorganize){
			reorganizeApplications();
	
			reorganize = false;
			
			scheduleQueue();
		}
	}

	@Override
	public void runApplication(Application app) {
		Boolean isScheduled = schedule(app);

		if (!isScheduled) {
			queue.add(app);
		}

	}

	@Override
	public void runApplication(Application app, Boolean queueing)
			throws SchedulerException {
		Boolean isScheduled = schedule(app);

		if (!isScheduled) {
			if (queueing) {
				queue.add(app);
			} else {
				throw new SchedulerException(
						"cannot schedule the Application. Not enough resources");
			}
		}
	}

	@Override
	public void update(Observable o, Object arg) {
//		PhysicalMachine pm = (PhysicalMachine) o;
//		if (0 == pm.getRunningVMCount()) {
//			pm.shutdown();
//			pms.remove(pm);
//
//			log.debug("Shutdown a Physical Machine - PM Count:" + pms.size());
//		}
//
//		// reorganize the remaining applications
//		reorganizeApplications();
//
		reorganize = true;
	}

	private void reorganizeApplications() {
		// sort the applications descending by CPU and reassign them to the VMs
		// shutdown free VMs/PMs
		
		ArrayList<Application> appsToQueue = new ArrayList<Application>();
		for (PhysicalMachine pm : pms) {
			appsToQueue.addAll(pm.getApplications());
		}
		
		Collections.sort(appsToQueue, appComp);
		//ArrayList<Application> appsToQueue = new ArrayList<Application>(queue);
		
		ArrayList<PhysicalMachine> pmsToRemove = new ArrayList<PhysicalMachine>();

		for (PhysicalMachine pm : pms) {
			pm.removeAllApplication();

			ArrayList<Application> appsToRemove = new ArrayList<Application>();
			Iterator<Application> it = appsToQueue.iterator();

			while (it.hasNext()) {
				Application app = it.next();

				int freeHdd = pm.getHdd() - pm.getHddInUse();
				int freeRam = pm.getRam() - pm.getRamInUse();
				int freeCpu = pm.getCpu() - pm.getCpuInUse();

				if (freeHdd >= app.getHdd() && freeRam >= app.getRam()
						&& freeCpu >= app.getCpu()) {
					try {
						if (pm.getRunningVMCount() > 0) {
							pm.increaseVMResources(app.getHdd(), app.getRam(),
									app.getCpu());
						} else {
							pm.startVM(app.getHdd(), app.getRam(), app.getCpu());
							
						}
						pm.addApplication(app);

						appsToRemove.add(app);

					} catch (MachineException e) {
						log.info(e.getMessage());
					}
				} else {

					break;
				}
			}
			
			if(appsToRemove.size()>0){

			appsToQueue.removeAll(appsToRemove);
			}else{
				//if (0 == pm.getRunningVMCount()) {
					pmsToRemove.add(pm);
				//}
				
			}

		}
		for (PhysicalMachine pm : pmsToRemove) {
			pm.shutdown();
			pms.remove(pm);

			log.debug("Shutdown a Physical Machine - PM Count:" + pms.size());
		}

	}

	private boolean schedule(Application app) {

		// no machine is running -> start a new PM with a VM
		if (0 < Settings.getPmCount() && pms.size() == 0) {
			PhysicalMachine pm = new PhysicalMachineImpl();
			((PhysicalMachineImpl) pm).addObserver(this);

			totalPms++;

			pms.add(pm);
		}

		Boolean isApplied = false;
		// find the "best" location
		// "best" location -> the PM with the less resources free

		PhysicalMachine pmBest = findBestPMToSchedule(app);

		if (pmBest != null) {
			try {
				if (pmBest.getRunningVMCount() > 0) {
					pmBest.increaseVMResources(app.getHdd(), app.getRam(),
							app.getCpu());
				} else {
					pmBest.startVM(app.getHdd(), app.getRam(), app.getCpu());
					totalVms++;
				}
				pmBest.addApplication(app);


				isApplied = true;
			} catch (MachineException e) {
				log.info(e.getMessage());
			}
		}

		if (isApplied) {
//			addLine(app, false);
			return true;
		}

		// no machine found -> start a new PM with a VM
		if (Settings.getPmCount() > pms.size()) {
			PhysicalMachine pm = new PhysicalMachineImpl();
			((PhysicalMachineImpl) pm).addObserver(this);
			pms.add(pm);

			totalPms++;

			try {
				pm.startVM(app.getHdd(), app.getRam(), app.getCpu());
				pm.addApplication(app);

				totalVms++;

				isApplied = true;
			} catch (MachineException e) {
				log.info(e.getMessage());
			}
		}

		if (isApplied) {
//			addLine(app, false);
			return true;
		}

		CloudPartner cp = new CloudPartner();
		for (int i = 0; i < Settings.getCloudPartnerCount(); i++) {
			if (cp.sendApplication(app)) {
//				addLine(app, true);

			//	totalDuration += app.getDuration();
				totalOutSourced++;

				return true;
			}
		}

		return false;
	}

	private PhysicalMachine findBestPMToSchedule(Application app) {
		int minFreeCPU = Integer.MAX_VALUE;
		PhysicalMachine machineToRet = null;

		Iterator<PhysicalMachine> iterator = pms.iterator();

		while (iterator.hasNext()) {
			PhysicalMachine pm = iterator.next();

			int freeHdd = pm.getHdd() - pm.getHddInUse();
			int freeRam = pm.getRam() - pm.getRamInUse();
			int freeCpu = pm.getCpu() - pm.getCpuInUse();

			if (freeHdd >= app.getHdd() && freeRam >= app.getRam()
					&& freeCpu >= app.getCpu()) {
				// enough resources
				if (freeCpu < minFreeCPU) {
					minFreeCPU = freeCpu;
					machineToRet = pm;
				}
			}
		}

		return machineToRet;
	}

	private void scheduleQueue() {
		Iterator<Application> iterator = queue.iterator();
		List<Application> rem = new ArrayList<Application>();
		while (iterator.hasNext()) {
			Application app = iterator.next();

			if (schedule(app)) {
				rem.add(app);
			}
		}

		queue.removeAll(rem);
	}

//	private void addLine(Application app, boolean outSourced) {
//		int totalHdd = 0;
//		int totalRam = 0;
//		int totalCpu = 0;
//		int runningVms = 0;
//		float totalPowerConsumption = 0;
//		int os = outSourced ? 1 : 0;
//
//		Iterator<PhysicalMachine> iterator = pms.iterator();
//		while (iterator.hasNext()) {
//			PhysicalMachine pm = iterator.next();
//
//			totalHdd += pm.getHddInUse();
//			totalRam += pm.getRamInUse();
//			totalCpu += pm.getCpuInUse();
//			runningVms += pm.getRunningVMCount();
//			totalPowerConsumption += pm.getPowerConsumtion();
//		}
//
//		SchedulerOutputFile sof = SchedulerOutputFile.getInstance();
//		SchedulerOutputRow sor = new SchedulerOutputRow(app.getTimestamp(),
//				totalRam, totalCpu, totalHdd, pms.size(), runningVms,
//				totalPowerConsumption, 0, os);
//
//		sof.addRow(sor);
//	}

}
